#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2011 - 2013 Stefano Mazzucco <stefano -at- curso.re>
# All rights reserved.
#
# This file is part of Crystal Ball Plus.
#
# Crystal Ball Plus is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Crystal Ball Plus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Crystal Ball Plus.  If not, see <http://www.gnu.org/licenses/>.

"""This module contains  various utility functions.

"""

import os.path
import numpy as np

def remove_duplicates(alist):
    """Remove duplicate elements IN PLACE from list 'alist'.

    Note that the elements of the list must support the method __eq__()

    *Parameters*

    alist : list

    *Returns*

    doubles : list
              list containing the duplicates that have been removed

    """
    i, j = 0, 1
    doubles = []
    while i < len(alist):
        while j < len(alist):
            if np.all(np.asarray(alist[i]) == np.asarray(alist[j])):
                doubles.append(alist.pop(j))
            else:
                j = j + 1
        j = i + 2
        i = i + 1
    return doubles

def percent_error(val, ref):
    """Return the percent error (with sign) between value 'val' and reference
    'ref'.

    *Parameters*

    val : float

    ref : float

    *Returns*

    percerr : float

    """

    if type(val) is not float:
        v = float(val)
    else:
        v = val
    if type(ref) is not float:
        r = float(ref)
    else:
        r = ref
    return (v - r) / r * 100

def match(inp, ref, err=None):
    """Matches the elements in two iterables (e.g. lists) that
    have minimum error (default) or within a given error.

    *Parameters*

    inp : iterable (list, tuple, array, etc.)
          input

    ref : iterable (list, tuple, array, etc.)
          reference

    err : float
          optional (e.g. 0.1 means 10%)
          if given all the elements in inp within err to ref will be considered

    *Returns*

    out : structured array with indexing keys: 'inp_idx', 'ref_idx', 'err'
          (i.e. input index, match reference index, error)
    """
    result = []
    for i in xrange(len(inp)):
        if err is None:
            dv = np.ones(len(ref))
            dv = dv * 1000
            for r in xrange(len(ref)):
                dv[r] = np.abs((inp[i] - ref[r]) / ref[r])
            result.append((i, np.argmin(dv), np.min(dv)))
        else:
            for r in xrange(len(ref)):
                dv = np.abs((inp[i] - ref[r]) / ref[r])
                if dv <= err:
                    result.append((i, r, dv))
    result = np.asarray(result, dtype={'names' : ['inp_idx',
                                                  'ref_idx',
                                                  'err'],
                                       'formats' : ['i4',
                                                    'i4',
                                                    'f4']})
    return result

def anglecount(N):
    """Return the total number of angles subtended to N vectors in a
    bidimensional space. This can also be applied to other problems.

    If you have N vectors, you will have::

       A = (N-1) + (N-2) + ... + 1 = Sum_(k=1)^(N-1){k} = (N - 1) N / 2

    """
    return (N - 1) * N / 2

def union(*lst):
    """Return a list with elements from all the other lists.

    *Parameters*

    *lst : lists

    *Returns*

    out : list of all elements in lists

    Examples

       >>> L1 = [ [0, 1, 2], [0, -1, -2], [0, 0, 3] ]
       >>> L2 = [ [0, -1, 2], [0, 1, -2], [0, 0, 3] ]
       >>> union(L1, L2)
           [[0, 1, 2], [0, -1, -2], [0, 0, 3], [0, -1, 2], [0, 1, -2]]

    """
    if len(lst) == 1:
        return lst[0]
    elif len(lst) == 2:
        out = []
        for i in lst[0]:
            out.append(i)
        for j in lst[1]:
            out.append(j)
        remove_duplicates(out)
        return out
    else:
        i = union(lst[0], lst[1])
        return union(i, *lst[2:])

def intersect(*lst):
    """Return the intersection of two or more lists (of arrays).

    *Parameters*

    *lst : lists

    *Returns*

    out : list of common elements among lists

    Examples

       >>> L1 = [ [0, 1, 2], [0, -1, -2], [0, 0, 3] ]
       >>> L2 = [ [0, -1, 2], [0, 1, -2], [0, 0, 3] ]
       >>> intersect(L1, L2)
           [ [0, 0, 3] ]
       >>> L3 = [ [1, 1, 2], [0, -1, -2], [0, 0, 3] ]
       >>> intersect(L1, L2, L3)
           [ [0, 0, 3] ]

    """
    if len(lst) == 1:
        return lst[0]
    elif len(lst) == 2:
        out = []
        for i in lst[0]:
            for j in lst[1]:
                if np.all(np.asarray(i) == np.asarray(j)):
                    out.append(i)
        return out
    else:
        i = intersect(lst[0], lst[1])
        return intersect(i, *lst[2:])

def difference(*lst):
    """Return a new list with elements in the first list that
    are not in the others.

    *Parameters*

    *lst : lists

    *Returns*

    out : list with elements in the first list that are not in the others

    Examples

       >>> L1 = [ [0, 1, 2], [0, -1, -2], [0, 0, 3] ]
       >>> L2 = [ [0, -1, 2], [0, 1, -2], [0, 0, 3] ]
       >>> difference(L1, L2)
           [[0, 1, 2], [0, -1, -2]]
       >>> difference(L2, L1)
           [[0, -1, 2], [0, 1, -2]]

    """
    if len(lst) == 1:
        return []
    elif len(lst) == 2:
        out = []
        for i in lst[0]:
            differ = 0
            for j in lst[1]:
                if not np.all(np.asarray(i) == np.asarray(j)):
                    differ += 1
            if differ == len(lst[1]):
                out.append(i)
        return out
    else:
        i = difference(lst[0], lst[1])
        return difference(i, *lst[2:])

def symmetric_difference(lst1, lst2):
    """Return a list with elements in either the list or other but not both.

    *Parameters*

    lst1 : list

    lst2 : list

    *Returns*

    out : list of elements in either list but not both

    Examples

       >>> L1 = [ [0, 1, 2], [0, -1, -2], [0, 0, 3] ]
       >>> L2 = [ [0, -1, 2], [0, 1, -2], [0, 0, 3] ]
       >>> symmetric_difference(L1, L2)
           [[0, 1, 2], [0, -1, -2], [0, -1, 2], [0, 1, -2]]

    """
    d1 = difference(lst1, lst2)
    d2 = difference(lst2, lst1)
    return union(d1, d2)

def genrow(lst, header, sep='    ', just='r'):
    """Return a string formatted to be fitted under the header

    *Parameters*

    lst : list of strings

    header : list of strings

    sep : string (optional)
          separator

    just : string (optional)
           justification; can be either of 'r', 'l', 'c' (right, left, center)
           default is 'r'

    *Returns*

    datalie : string

    """
    j = {'r' : str.rjust, 'l' : str.ljust, 'c' : str.center}
    if just not in j:
        raise ValueError('Wong justification parameter: %s' % just)
    else:
        just = j[just]
    if len(lst) != len(header):
        print ('input and header sizes do not match')
        return
    else:
        dataline = ''
        for h, i in zip(header, lst):
            dataline += just(i, len(h)) + sep
        return dataline

def merge_files(out, *files):
    """Merge text files in a single one. If the output files already exists, it
    will be overwritten. Also, non-existing files will be skipped.

    *Parameters*

    out : string
          output file

    *files : list of strings
             input files

    *Returns*

    nothing

    *Example*

    merge_files('/home/user/result.txt', './file1.txt', './file2.txt', './file3.txt')

    """
    with open(out, 'w') as f:
        for merge in files:
            if os.path.isfile(merge):
                with open(merge) as m:
                    f.write(m.readline())
    return
